package com.sikaryofficial.file.service;

import cn.hutool.core.text.CharSequenceUtil;
import com.google.common.collect.Lists;
import com.sikaryofficial.common.core.exception.ServiceException;
import com.sikaryofficial.common.core.utils.file.FileUtils;
import com.sikaryofficial.common.core.utils.file.MimeTypeUtils;
import com.sikaryofficial.file.config.FfmpegConfig;
import com.sikaryofficial.file.config.MinioConfig;
import com.sikaryofficial.file.utils.FileUploadUtils;
import com.sikaryofficial.system.api.domain.SysFile;
import com.sikaryofficial.system.api.model.FileMergeReq;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.errors.ErrorResponseException;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.InvalidResponseException;
import io.minio.errors.ServerException;
import io.minio.errors.XmlParserException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author : qinjinyuan
 * @desc : Ffmpeg视频加工处理
 * @date : 2024/06/28 8:46
 */
@Component
@Slf4j
public class FfmpegProcess {
    private static final char DOT = '.';
    @Autowired
    private MinioConfig minioConfig;
    @Autowired
    private MinioClient client;
    @Autowired
    private FfmpegConfig ffmpegConfig;

    /**
     * 视频和音频合并
     * @param req
     * @return
     * @throws Exception
     */
    public SysFile videoAndAudioMerge(FileMergeReq req) throws Exception {
        if (CharSequenceUtil.isBlank(req.getAudioPath())) {
            // 如果音频文件是空，则直接上传视频
            return uploadVideo(req.getVideoFile(), req);
        }
        MultipartFile multipartFile = req.getVideoFile();
        String sourceLocalPath = MessageFormat.format(ffmpegConfig.getSourceLocalPath(), String.valueOf(System.nanoTime()));
        File filePath = new File(sourceLocalPath);
        if (filePath.isDirectory()) {
            filePath.mkdirs();
        }
        String fileName = FileUploadUtils.extractFilename(multipartFile);
        String mergeFileName = fileName.substring(0, fileName.lastIndexOf(DOT)) + "_merge"
                + fileName.substring(fileName.lastIndexOf(DOT));
        // 1、将 multipartFile 落地到本地指定目录，
        String sourceFilePath = sourceLocalPath + File.separator + fileName;
        FileUtils.convertMultipartFileToFile(multipartFile, sourceFilePath);
        // 2、获取音频文件路径
        String audioFilePath = buildAudioPath(req, sourceFilePath);
        // 3、合并音视频
        mergeAudioVideo(req, sourceLocalPath, sourceFilePath, audioFilePath, mergeFileName);
        // 4、上传合并视频
        String mergeFFilePath = sourceLocalPath + File.separator + mergeFileName;
        return transferAndUpload(multipartFile, sourceLocalPath, mergeFileName, mergeFFilePath, req.getVideoPicStartTime());
    }

    /**
     * 1、则进行 ts 转换
     * 2、ts 切片 并生成 索引文件m3u8
     * 3、把所有本地处理后的文件，上传到minio
     * 4、返回m3u8地址
     */
    public SysFile uploadVideo(MultipartFile multipartFile) throws Exception {
        String sourceLocalPath = MessageFormat.format(ffmpegConfig.getSourceLocalPath(), String.valueOf(System.nanoTime()));
        File filePath = new File(sourceLocalPath);
        if (filePath.isDirectory()) {
            filePath.mkdirs();
        }
        String fileName = FileUploadUtils.extractFilename(multipartFile);
        // 1、将 multipartFile 落地到本地指定目录，
        String sourceFile = sourceLocalPath + File.separator + fileName;
        log.info("sourceFile文件路径：{}", sourceFile);
        FileUtils.convertMultipartFileToFile(multipartFile, sourceFile);
        // 2、文件格式校验
        FileUploadUtils.assertAllowedWithExt(multipartFile, MimeTypeUtils.DEFAULT_ALLOWED_VIDEO_EXTENSION);
        return transferAndUpload(multipartFile, sourceLocalPath, fileName, sourceFile,null);
    }

    public SysFile uploadVideo(MultipartFile multipartFile, FileMergeReq req) throws Exception {
        String sourceLocalPath = MessageFormat.format(ffmpegConfig.getSourceLocalPath(), String.valueOf(System.nanoTime()));
        File filePath = new File(sourceLocalPath);
        if (filePath.isDirectory()) {
            filePath.mkdirs();
        }
        String fileName = FileUploadUtils.extractFilename(multipartFile);
        // 1、将 multipartFile 落地到本地指定目录，
        String sourceFile = sourceLocalPath + File.separator + fileName;
        log.info("sourceFile文件路径：{}", sourceFile);
        FileUtils.convertMultipartFileToFile(multipartFile, sourceFile);
        // 2、文件格式校验
        FileUploadUtils.assertAllowedWithExt(multipartFile, MimeTypeUtils.DEFAULT_ALLOWED_VIDEO_EXTENSION);
        // 3、获取文件播放时长:单位秒
        String duration = getDurationByPath(sourceFile);
        if ("N/A".equals(duration)) {
            duration = req.getAudioDuration().toString();
        }
        double dur = Double.parseDouble(duration);
        if (dur > ffmpegConfig.getMaxDuration()) {
            throw new ServiceException("Duration must not exceed 30 seconds, but got: " + dur);
        }
        return transferAndUpload1(multipartFile, sourceLocalPath, fileName, sourceFile, req.getVideoPicStartTime(), duration);
    }

    private SysFile transferAndUpload(MultipartFile multipartFile, String sourceLocalPath, String fileName, String sourceFile,
                                      String videoPicStartTime) {
        // 3、获取文件播放时长:单位秒
        String duration = getDurationByPath(sourceFile);
        if ("N/A".equals(duration)) {
            throw new ServiceException("Video file parsing failed, please check the video file");
        }
        double dur = Double.parseDouble(duration);
        if (dur > ffmpegConfig.getMaxDuration()) {
            throw new ServiceException("Duration must not exceed 30 seconds, but got: " + dur);
        }
        return transferAndUpload1(multipartFile, sourceLocalPath, fileName, sourceFile, videoPicStartTime, duration);
    }

    private SysFile transferAndUpload1(MultipartFile multipartFile, String sourceLocalPath, String fileName, String sourceFile,
                                       String videoPicStartTime, String duration) {
        // 3、生成封面
        genCoverPic(sourceFile, videoPicStartTime);
        // 4、转Source为TS文件
        String outputTSFile = processSourceToTSFile(fileName, sourceFile, sourceLocalPath);
        // 5、对TS文件进行切片，并生成m3u8索引文件
        processTsToM3u8File(fileName, outputTSFile, sourceLocalPath, calculateChunkDuration(duration) + "");
        // 6、上传文件
        SysFile sysFile = uploadAllFile(multipartFile, outputTSFile, sourceLocalPath);
        sysFile.setDuration(new BigDecimal(duration));
        sysFile.setFileSize(FileUploadUtils.formatFileSizeMB(new File(sourceFile).length()));
        return sysFile;
    }

    private int calculateChunkDuration(String duration) {
        double videoDuration = Double.parseDouble(duration);
        if (videoDuration <= 30) {
            // 30 秒以内的视频，每个分片 2 秒
            return 2;
        } else {
            // 超过30 秒的视频，控制分片文件个数在30以内
            double chunkDurationDouble = videoDuration / 30;
            // 四舍五入
            return (int) Math.round(chunkDurationDouble);
        }
    }

    /**
     * 无损音频文件压缩
     *
     * @param multipartFile
     * @return
     */
    public SysFile uploadAudio(MultipartFile multipartFile) {
        String sourceLocalPath = MessageFormat.format(ffmpegConfig.getSourceLocalPath(), String.valueOf(System.nanoTime()));
        File filePath = new File(sourceLocalPath);
        if (filePath.isDirectory()) {
            filePath.mkdirs();
        }
        String fileName = FileUploadUtils.extractFilename(multipartFile);
        // 1、将 multipartFile 落地到本地指定目录，
        String sourceFile = sourceLocalPath + File.separator + fileName;
        // 1.1 落地到服务器
        FileUtils.convertMultipartFileToFile(multipartFile, sourceFile);
        // 1.2 音频文件压缩，并保存原有格式
        String compressionFile = processAudioFileCompression(fileName, sourceFile, sourceLocalPath);
        // 2、转Source为OGG文件
        String oggFile = processSourceToOGGFile(fileName, sourceFile, sourceLocalPath);
        // 3、获取文件播放时长:单位秒
        String duration = getDurationByPath(oggFile);
        // 4、上传文件
        String uploadPath = uploadAudioFile(multipartFile.getContentType(), sourceLocalPath, oggFile);
        String uploadCompressionPath = uploadAudioFile(multipartFile.getContentType(), sourceLocalPath, compressionFile);

        SysFile sysFile = new SysFile();
        sysFile.setUrl(minioConfig.getBucketName() + File.separator + uploadPath);
        sysFile.setLocalOriginalLink(minioConfig.getBucketName() + File.separator + uploadCompressionPath);
        sysFile.setName(FileUtils.getName(sysFile.getUrl()));
        sysFile.setFileSize(FileUploadUtils.formatFileSizeMB(new File(oggFile).length()));
        sysFile.setDuration(new BigDecimal(duration));
        return sysFile;
    }

    private String uploadAudioFile(String contentType, String sourceLocalPath, String oggFile) {
        String uploadPath = oggFile.replaceFirst(sourceLocalPath, "");
        uploadPath = uploadPath.startsWith(File.separator) ? uploadPath.substring(File.separator.length()) : uploadPath;
        log.info("uploadPath:{}", uploadPath);
        // 上传文件
        File file = new File(oggFile);
        try (InputStream stream = Files.newInputStream(file.toPath())) {
            PutObjectArgs args = PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(uploadPath).stream(stream, file.length(), -1).contentType(contentType).build();
            client.putObject(args);
        } catch (IOException | ErrorResponseException | InsufficientDataException | InternalException |
                 InvalidKeyException | InvalidResponseException | NoSuchAlgorithmException | ServerException |
                 XmlParserException e) {
            log.error("上传文件失败{}", e);
        }
        return uploadPath;
    }

    /**
     * 视频封面处理
     *
     * @param sourceFile
     */
    private void genCoverPic(String sourceFile, String videoPicStartTime) {
        if (CharSequenceUtil.isBlank(videoPicStartTime)) {
            videoPicStartTime = "00:00:01";
        }
        // 切封面 （图片位置在脚本中定义 默认png）
        executeCommandForScript(Arrays.asList("sh", ffmpegConfig.getCoverPicForVideo(), sourceFile, videoPicStartTime));
    }

    private void mergeAudioVideo(FileMergeReq req, String sourceLocalPath, String videoSourceFile, String audioSourceFile,
                                 String mergeFileName) {
        String mergeFile = buildTargetFilePath(sourceLocalPath, mergeFileName,
                mergeFileName.substring(mergeFileName.lastIndexOf(DOT) + 1));
        // 验证输入参数
        validateInput(req, videoSourceFile, audioSourceFile);
        executeCommandForScript(Arrays.asList("sh", ffmpegConfig.getMergeAudioVideo(), videoSourceFile
                , audioSourceFile, mergeFile, req.getAudioStartTime().toString(), req.getAudioEndTime().toString()));
    }

    // 验证输入参数的方法
    private void validateInput(FileMergeReq req, String videoSourceFile, String audioSourceFile) {
        if (req == null || videoSourceFile == null || audioSourceFile == null) {
            throw new IllegalArgumentException("Input parameters cannot be null.");
        }
        // 更多验证逻辑...
    }

    private String buildAudioPath(FileMergeReq req, String sourceFilePath) {
        String audioURL = minioConfig.getUrl() + File.separator + req.getAudioPath();
        // 创建一个临时文件或指定的文件路径
        String[] audioTmpFilePath = req.getAudioPath().split(File.separator, -1);
        String audioFileName = audioTmpFilePath[audioTmpFilePath.length - 1];
        File sourceFile = new File(sourceFilePath);
        String audioFilePath = sourceFile.getParent() + File.separator + audioFileName;
        Path sourceAudioFilePath = Paths.get(audioFilePath);
        try (InputStream in = new URL(audioURL).openStream()) {
            // 将内容写入到本地文件
            Files.copy(in, sourceAudioFilePath);
        } catch (IOException e) {
            // 处理异常
            e.printStackTrace();
        }
        return audioFilePath;
    }

    /**
     * 转换为TS文件
     *
     * @param fileName
     * @param sourceFile
     * @return
     */
    private String processSourceToTSFile(String fileName, String sourceFile, String sourceLocalPath) {
        String tSFile = buildTargetFilePath(sourceLocalPath, fileName, MimeTypeUtils.TS);
        executeCommandForScript(Arrays.asList("sh", ffmpegConfig.getConvertFileToTs(), sourceFile, tSFile));
        return tSFile;
    }

    private String processSourceToOGGFile(String fileName, String sourceFile, String sourceLocalPath) {
        if (sourceFile.endsWith(MimeTypeUtils.OGG)) {
            return sourceFile;
        }
        String tSFile = buildTargetFilePath(sourceLocalPath, fileName, MimeTypeUtils.OGG);
        executeCommandForScript(Arrays.asList("sh", ffmpegConfig.getConvertAudioToOgg(), sourceFile, tSFile));
        return tSFile;
    }

    private String processAudioFileCompression(String fileName, String sourceFile, String sourceLocalPath) {
        String tSFile = buildTargetFilePath1(sourceLocalPath, fileName);
        executeCommandForScript(Arrays.asList("sh", ffmpegConfig.getAudioCompression(), sourceFile, tSFile));
        return tSFile;
    }

    /**
     * TS文件切片并生成索引文件m3u8
     *
     * @param fileName
     * @param outputTSFile 输出文件名
     *
     * @param segmentTime 分片文件时长
     */
    private void processTsToM3u8File(String fileName, String outputTSFile, String sourceLocalPath, String segmentTime) {
        // TS文件切片并生成索引文件m3u8
        String m3u8File = buildTargetFilePath(sourceLocalPath, fileName, MimeTypeUtils.M3U8);
        // 切片文件名
        String fragmentationFile = buildTargetFilePath(sourceLocalPath, fileName, MimeTypeUtils.TS, "-%04d");
        executeCommandForScript(Arrays.asList("sh", ffmpegConfig.getConvertTsToTs(), outputTSFile, m3u8File, fragmentationFile, segmentTime));
    }

    /**
     * 构建目标文件路径
     *
     * @param sourceLocalPath 配置的基础本地路径
     * @param fileName        输入落地文件名称
     * @param suffix          文件后缀
     * @return
     */
    private String buildTargetFilePath(String sourceLocalPath, String fileName, String suffix) {
        int extensionIndex = fileName.indexOf(DOT);
        // 确保文件名中有'.'来定位扩展名
        if (extensionIndex == -1) {
            throw new IllegalArgumentException("File name must contain an extension.");
        }
        Path basePath = Paths.get(sourceLocalPath, fileName.substring(0, extensionIndex + 1));
        return basePath + suffix;
    }

    private String buildTargetFilePath1(String sourceLocalPath, String fileName) {
        int extensionIndex = fileName.indexOf(DOT);
        // 确保文件名中有'.'来定位扩展名
        if (extensionIndex == -1) {
            throw new IllegalArgumentException("File name must contain an extension.");
        }
        // 生成10以内的随机数
        int randomNum = new Random().nextInt(10);
        String randomStr = Integer.toString(randomNum);
        Path basePath = Paths.get(sourceLocalPath, fileName.substring(0, extensionIndex + 1)+randomStr+ fileName.substring(extensionIndex));
        return basePath.toString() ;
    }
    /**
     * 构建目标文件路径
     *
     * @param sourceLocalPath 配置的基础本地路径
     * @param fileName        输入落地文件名称
     * @param suffix          文件后缀
     * @param splitFormat     文件名中的插入格式，可选。
     * @return 完成构建的目标文件路径。
     */
    private String buildTargetFilePath(String sourceLocalPath, String fileName, String suffix, String splitFormat) {
        int extensionIndex = fileName.indexOf(DOT);
        // 确保文件名中有'.'来定位扩展名
        if (extensionIndex == -1) {
            throw new IllegalArgumentException("File name must contain an extension.");
        }
        Path basePath = Paths.get(sourceLocalPath, fileName.substring(0, extensionIndex));
        StringBuilder pathBuilder = new StringBuilder(basePath.toString());
        if (StringUtils.isNotBlank(splitFormat)) {
            pathBuilder.append(splitFormat).append(DOT);
        }
        pathBuilder.append(suffix);
        return pathBuilder.toString();
    }

    private SysFile uploadAllFile(MultipartFile multipartFile, String outputTSFile, String sourceLocalPath) {
        SysFile sysFile = new SysFile();
        // 判断该路径是否存在且为目录
        List<String> uploadFilePaths = getAllUploadFileInfo(outputTSFile);
        AtomicReference<String> atomicReferenceM3u8 = new AtomicReference<>("");
        AtomicReference<String> atomicReferenceCoverPic = new AtomicReference<>("");
        uploadFilePaths.forEach(item -> {
            // log.info("item:{}", item);
            String uploadPath = item.replaceFirst(sourceLocalPath, "");
            uploadPath = uploadPath.startsWith(File.separator) ? uploadPath.substring(File.separator.length()) : uploadPath;
            log.info("uploadPath:{}", uploadPath);
            if (item.endsWith(MimeTypeUtils.M3U8)) {
                atomicReferenceM3u8.set(uploadPath);
            }
            if (item.endsWith(MimeTypeUtils.PNG) || item.endsWith(MimeTypeUtils.JPG)) {
                atomicReferenceCoverPic.set(uploadPath);
            }
            // 上传文件
            File file = new File(item);
            try (InputStream stream = Files.newInputStream(file.toPath())) {
                PutObjectArgs args = PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(uploadPath).stream(stream, file.length(), -1).contentType(multipartFile.getContentType()).build();
                client.putObject(args);
            } catch (IOException | ErrorResponseException | InsufficientDataException | InternalException |
                     InvalidKeyException | InvalidResponseException | NoSuchAlgorithmException | ServerException |
                     XmlParserException e) {
                log.error("上传文件失败{}", e);
            }
        });
        sysFile.setVideoCoverUrl(minioConfig.getBucketName() + File.separator + atomicReferenceCoverPic.get());
        sysFile.setUrl(minioConfig.getBucketName() + File.separator + atomicReferenceM3u8.get());
        sysFile.setName(FileUtils.getName(sysFile.getUrl()));
        return sysFile;
    }

    private List<String> getAllUploadFileInfo(String outputTSFile) {
        List<String> uploadFilePaths = Lists.newArrayList();
        String path = outputTSFile.substring(0, outputTSFile.lastIndexOf(File.separator));
        log.info("path:{}", path);
        File currentFiles = new File(path);
        if (currentFiles.isDirectory()) {
            // 获取该目录下的所有文件和子目录
            File[] files = currentFiles.listFiles();
            if (files != null) {
                // 遍历文件数组
                Arrays.stream(files).filter(file -> file.getName().endsWith(MimeTypeUtils.M3U8) ||
                        file.getName().endsWith(MimeTypeUtils.TS) ||
                        file.getName().endsWith(MimeTypeUtils.PNG) ||
                        file.getName().endsWith(MimeTypeUtils.JPG)).forEach(file -> {
                    if (!Objects.equals(file.getAbsolutePath(), outputTSFile)) {
                        // log.info("file.getAbsolutePath():{}", file.getAbsolutePath());
                        uploadFilePaths.add(file.getAbsolutePath());
                    }
                });
            }
        }
        return uploadFilePaths;
    }

    private void executeCommandForScript(List<String> command) {
        // 使用ProcessBuilder执行命令
        try {
            log.info("---------->>>>command:{}", StringUtils.join(command, " "));
            ProcessBuilder pb = new ProcessBuilder(command);
            Process process = pb.start();
            // 获取并打印命令的输出
            //Thread stdoutThread = readStream(process.getInputStream(), "OUTPUT");
            //Thread stderrThread = readStream(process.getErrorStream(), "ERROR");
            //stdoutThread.start();
            //stderrThread.start();
            // 等待命令执行完成
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                log.info("Video conversion completed successfully.");
            } else {
                log.error("Video conversion failed with exit code: " + exitCode);
            }
            // 确保读取线程完成
            //stdoutThread.join();
            //stderrThread.join();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private String getDurationByPath(String filePath) {
        // 构建 ffprobe 命令
        String[] command = {"ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filePath};
        try {
            // 创建 ProcessBuilder 实例
            ProcessBuilder processBuilder = new ProcessBuilder(command);
            // 启动进程
            Process process = processBuilder.start();
            // 读取标准输出流
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                // 读取输出并解析持续时间
                String line;
                while ((line = reader.readLine()) != null) {
                    try {
                        return line.trim();
                    } catch (NumberFormatException e) {
                        System.err.println("Failed to parse duration from line: " + line);
                    }
                }
            }
        } catch (IOException e) {
            log.error("Error executing ffprobe: " + e.getMessage());
        }
        // 如果没有读取到有效的持续时间，则返回 -1 表示失败
        return "0";
    }

    private void genCoverPicNew(String sourceFile) {
        String targetFile = sourceFile.substring(0, sourceFile.lastIndexOf('.')) + ".png";
        String[] command = {"ffmpeg", "-y","-i", sourceFile, "-vframes 1", targetFile};
        try {
            log.info("---------->>>>command:{}", StringUtils.join(command, " "));
            // 创建 ProcessBuilder 实例
            ProcessBuilder processBuilder = new ProcessBuilder(command);
            // 启动进程
            Process process = processBuilder.start();
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                log.info("genCoverPic completed successfully.");
            } else {
                log.error("genCoverPic failed with exit code: " + exitCode);
            }
        } catch (IOException | InterruptedException e) {
            log.error("Error executing ffprobe: " + e.getMessage());
        }
    }

    private void genCoverPicNew1(String sourceFile) {
        String targetFile = sourceFile.substring(0, sourceFile.lastIndexOf('.')) + ".jpg";
        String[] command = {"ffmpeg", "-y","-i", sourceFile, "-vframes 1", "-an -f image2", targetFile};
        try {
            String commandStr = StringUtils.join(command, " ");
            log.info("---------->>>>command:{}", commandStr);
            // 执行命令
            Process process = Runtime.getRuntime().exec(commandStr);
            // 读取标准输出
            BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
            // 读取错误输出
            BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            // 输出标准输出
            log.info("Here is the standard output of the command:");
            String s = null;
            while ((s = stdInput.readLine()) != null) {
                log.info(s);
            }
            // 输出错误输出
            System.out.println("Here is the standard error of the command (if any):");
            while ((s = stdError.readLine()) != null) {
                log.info(s);
            }
            // 等待命令执行完毕
            int exitVal = process.waitFor();
            if (exitVal == 0) {
                log.info("Command executed successfully.");
            } else {
                log.info("Command failed with exit code: " + exitVal);
            }
            // 关闭资源
            stdInput.close();
            stdError.close();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }


}
